package com.tngames.angrytunisian.game.core.entity;

import static playn.core.PlayN.graphics;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;

import org.jbox2d.callbacks.ContactImpulse;
import org.jbox2d.callbacks.ContactListener;
import org.jbox2d.callbacks.DebugDraw;
import org.jbox2d.collision.Manifold;
import org.jbox2d.collision.shapes.PolygonShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.World;
import org.jbox2d.dynamics.contacts.Contact;

import com.tngames.angrytunisian.game.core.Constants;
import com.tngames.angrytunisian.game.core.env.Terrain;

import playn.core.CanvasImage;
import playn.core.CanvasLayer;
import playn.core.DebugDrawBox2D;
import playn.core.GroupLayer;
import playn.core.gl.CanvasLayerGL;

public class EntityEngine implements ContactListener {

	// box2d object containing physics world
	private final World world;
	private Body groundBody;

	private static float groundHeight = 3.0f;

	public GroupLayer staticLayerBack;
	public GroupLayer dynamicLayer;
	public GroupLayer staticLayerFront;

	private DebugDrawBox2D debugDraw;
	private boolean dEBUG_DRAW;

	protected final GroupLayer worldlayer;
	private final List<Entity> entities = new ArrayList<Entity>(0);
	private final HashMap<Body, PhysicsEntity> bodyEntityLUT = new HashMap<Body, PhysicsEntity>();
	private final Stack<Entity> entitiesToRemove = new Stack<Entity>();

	private final Stack<Contact> contacts = new Stack<Contact>();

	private final Stack<Entity> entitiesToAdd = new Stack<Entity>();

	public EntityEngine(GroupLayer worldlayer, Vec2 gravity, int i, int j,
			float pHYS_UNIT_PER_SCREEN_UNIT, boolean dEBUG_DRAW) {
		this.dEBUG_DRAW = dEBUG_DRAW;
		this.worldlayer = worldlayer;
		staticLayerBack = graphics().createGroupLayer();
		worldlayer.add(staticLayerBack);

		dynamicLayer = graphics().createGroupLayer();
		worldlayer.add(dynamicLayer);

		staticLayerFront = graphics().createGroupLayer();
		worldlayer.add(staticLayerFront);

		// create the physics world
		world = new World(gravity, true);
		world.setWarmStarting(true);
		world.setAutoClearForces(true);
		world.setContactListener(this);

		// box2D debug mode
		if (dEBUG_DRAW) {
			CanvasImage canvasLayer = graphics().createImage(640, 480);
			graphics().rootLayer()
					.add(graphics().createImageLayer(canvasLayer));
			debugDraw = new DebugDrawBox2D();
			debugDraw.setCanvas(canvasLayer);
			debugDraw.setFlipY(false);
			debugDraw.setStrokeAlpha(150);
			debugDraw.setFillAlpha(75);
			debugDraw.setStrokeWidth(2.0f);
			debugDraw.setFlags(DebugDraw.e_shapeBit | DebugDraw.e_jointBit
					| DebugDraw.e_aabbBit);
			debugDraw.setCamera(0, 0, 1f / Constants.PHYS_UNIT_PER_SCREEN_UNIT);
			world.setDebugDraw(debugDraw);
		}
		Body groundBody = world.createBody(new BodyDef());
		PolygonShape groundShape = new PolygonShape();
		groundShape.setAsEdge(new Vec2(0, Constants.WORLD_HEIGHT - 1),
				new Vec2(Constants.WORLD_WIDTH, Constants.WORLD_HEIGHT - 1));
		groundBody.createFixture(groundShape, 0.0f);
//		Terrain terrain = new Terrain(this, groundShape);
//		this.add(terrain);
	}

	public Body getGroundBody() {
		return groundBody;
	}

	public void setGroundBody(Body groundBody) {
		this.groundBody = groundBody;
	}

	public void add(Entity entity) {
		entitiesToAdd.push(entity);
	}

	public void remove(Entity entity) {
		entitiesToRemove.push(entity);
	}

	public void update(float delta) {
		
		if (dEBUG_DRAW) {
			debugDraw.getCanvas().clear();
			world.drawDebugData();
		}
		
		for (Entity e : entities) {
			e.update(delta);
		}

		while (!entitiesToAdd.isEmpty()) {
			Entity entity = entitiesToAdd.pop();
			doAdd(entity);
		}

		while (!entitiesToRemove.isEmpty()) {
			Entity entity = entitiesToRemove.pop();
			doRemove(entity);
		}

		// the step delta is fixed so box2d isn't affected by framerate
		world.step(0.033f, 10, 10);
		processContacts();
	}

	public void doAdd(Entity entity) {
		entities.add(entity);
		if (entity instanceof PhysicsEntity) {
			PhysicsEntity physicsEntity = (PhysicsEntity) entity;
			bodyEntityLUT.put(physicsEntity.getBody(), physicsEntity);
		}
	}

	private void doRemove(Entity entity) {
		entities.remove(entity);
		entity.getLayer().destroy();
		if (entity instanceof PhysicsEntity) {
			PhysicsEntity physicsEntity = (PhysicsEntity) entity;
			Body body = physicsEntity.getBody();
			bodyEntityLUT.remove(body);
			world.destroyBody(body);
		}
	}

	public void processContacts() {
		while (!contacts.isEmpty()) {
			Contact contact = contacts.pop();
			// handle collision
			PhysicsEntity entityA = bodyEntityLUT
					.get(contact.m_fixtureA.m_body);
			PhysicsEntity entityB = bodyEntityLUT
					.get(contact.m_fixtureB.m_body);
			if (entityA instanceof PhysicsEntity.HasContactListener) {
				((PhysicsEntity.HasContactListener) entityA).contact(entityB);
			}
			if (entityB instanceof PhysicsEntity.HasContactListener) {
				((PhysicsEntity.HasContactListener) entityB).contact(entityA);
			}
		}
	}

	@Override
	public void beginContact(Contact contact) {
		// TODO Auto-generated method stub

	}

	@Override
	public void endContact(Contact contact) {
		contacts.push(contact);
	}

	@Override
	public void preSolve(Contact contact, Manifold oldManifold) {
		PhysicsEntity entityA = bodyEntityLUT.get(contact.m_fixtureA.m_body);
		PhysicsEntity entityB = bodyEntityLUT.get(contact.m_fixtureB.m_body);

		if (entityA != null && entityB != null) {
			if (entityA instanceof PhysicsEntity.HasPresolveListener) {
				((PhysicsEntity.HasPresolveListener) entityA).presolve(contact,
						entityB);
			}
			if (entityB instanceof PhysicsEntity.HasPresolveListener) {
				((PhysicsEntity.HasPresolveListener) entityB).presolve(contact,
						entityA);
			}
		}
	}

	@Override
	public void postSolve(Contact contact, ContactImpulse impulse) {
		// TODO Auto-generated method stub

	}

	public World getWorld() {
		return world;
	}

	public GroupLayer getWorldLayer() {
		return worldlayer;
	}

}
